package com.collins.io.nio.receive;

import com.collins.io.nio.config.Config;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * 客户端每次传输文件都会分为多次传输：首先传入文件名称，其次是文件大小，然后是文件内容。
 * 对于每一个客户端socketChannel，创建一个客户端对象，用于保存客户端状态，
 * 分别保存文件名、文件大小和写入的目标文件通道outChannel。
 * socketChannel和Client对象之间是一对一的对应关系：建立连接时，在Map中以键-值对的形式保存Client实例，
 * 其中socketChannel作为键(Key)，Client对象作为值(Value)。
 * 当socketChannel传输通道有数据可读时，通过选择键key.channel()方法取出IO事件所在的socketChannel通道，
 * 然后通过socketChannel通道从map中获取对应的Client对象。
 * 接收到数据时，如果文件名为空，就先处理文件名称，并把文件名保存到Client对象，同时创建服务器上的目标文件；
 * 接下来读取到数据，说明接收到了文件大小，把文件大小保存到Client对象；
 * 接下来接收到数据，说明是文件内容，写入Client对象的outChannel文件通道中，直到数据读取完毕。
 * 运行方式是先启动NioReceiveServer服务器程序，再启动前面介绍的客户端程序NioSendClient，完成文件的传输。
 */
public class ReceiveServer {
    private static final String RECEIVE_PATH = Config.SOCKET_RECEIVE_PATH;
    private static final Logger logger = Logger.getLogger(ReceiveServer.class);
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    // private Charset charset = Charset.forName("UTF-8");
    private Charset charset = StandardCharsets.UTF_8;

    public static void main(String[] args) throws IOException {
        ReceiveServer server = new ReceiveServer();
        server.startServer();
    }

    /**
     * INFO one client object corresponds to one client connection
     */
    static class Client {
        // INFO 文件名称
        String fileName;
        // INFO 文件长度
        long fileLength;
        // INFO 开始传输时间
        long startTime;
        // INFO 客户端地址
        InetSocketAddress remoteAddress;
        // INFO 输出文件通道
        FileChannel outChannel;
        // INFO 接收长度
        long receiveLength;

        // INFO 判断是否接收完毕
        public boolean isFinished() {
            return receiveLength >= fileLength;
        }
    }

    // INFO prepare server byte buffer
    private ByteBuffer buffer = ByteBuffer.allocate(Config.SERVER_BUFFER_SIZE);
    // INFO store client object for each client connection
    private Map<SelectableChannel, Client> clients = new HashMap<>();

    public void startServer() throws IOException {
        // TODO 1
        Selector selector = Selector.open();
        // TODO 2
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        InetSocketAddress address = new InetSocketAddress(Config.SERVER_PORT);
        serverSocketChannel.bind(address);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        logger.info("Server started listening on port " + Config.SERVER_PORT);
        // TODO 3 important - polling
        while (selector.select() > 0) {
            //TODO 4
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                // TODO 5 important - switching
                SelectionKey key = iterator.next();
                // INFO 如果是新连接，则获取连接
                if (key.isAcceptable()) {
                    // INFO 连接处理
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel channel = server.accept();
                    if (channel == null) continue;
                    channel.configureBlocking(false);
                    SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_READ);
                    // INFO 业务处理
                    Client client = new Client();
                    client.remoteAddress = (InetSocketAddress) channel.getRemoteAddress();
                    clients.put(channel, client);
                    logger.info(sdf.format(new Date(System.currentTimeMillis())) +
                            " channel connected: " + channel.getRemoteAddress());
                } else if (key.isReadable()) {
                    processData(key);
                }
                iterator.remove();
            }
        }
    }

    public void processData(SelectionKey key) throws IOException {
        Client client = clients.get(key.channel());
        SocketChannel channel = (SocketChannel) key.channel();
        int num = 0;
        try {
            buffer.clear();
            while ((num = channel.read(buffer)) > 0) {
                buffer.flip();
                // INFO 保证是第一次读，查看是否已经被系统记录
                if (null == client.fileName) {
                    // INFO 读文件名长度
                    if (buffer.capacity() < 4) {
                        continue;
                    }
                    int fileNameLength = buffer.getInt();
                    byte[] fileNameBytes = new byte[fileNameLength];
                    // INFO 读文件名
                    buffer.get(fileNameBytes);
                    String fileName = new String(fileNameBytes, charset);
                    // INFO 本地创建文件
                    File directory = new File(Config.SOCKET_RECEIVE_PATH);
                    if (!directory.exists()) {
                        directory.mkdir();
                    }
                    // LOG INFOS file directory
                    logger.info(sdf.format(new Date(System.currentTimeMillis())) +
                            " NIO sending directory: " + directory.getAbsolutePath());
                    client.fileName = fileName;
                    String fullName = directory.getAbsolutePath() + File.separatorChar + fileName;
                    // LOG DEBUG file name path
                    logger.debug(sdf.format(new Date(System.currentTimeMillis())) +
                            " NIO sending file: " + fullName);
                    File file = new File(fullName.trim());
                    if (!file.exists()) {
                        file.createNewFile();
                    }
                    // INFO 创立文件输出流
                    FileChannel fileChannel = new FileOutputStream(file).getChannel();
                    client.outChannel = fileChannel;
                    if (buffer.capacity() < 8) {
                        continue;
                    }
                    // INFO 读文件长度
                    client.fileLength = buffer.getLong();
                    client.startTime = System.currentTimeMillis();
                    // LOG
                    logger.debug(sdf.format(new Date(System.currentTimeMillis())) +
                            " NIO start sending file: " + fullName);
                    // INFO 读文件 - 在读完基本信息后的第一次读
                    client.receiveLength += buffer.capacity();
                    if (buffer.capacity() > 0) {
                        client.outChannel.write(buffer);
                    }
                    if (client.isFinished()) {
                        finished(key, client);
                    }
                    buffer.clear();
                } else {
                    // INFO 读文件内容 - 一次没读完，在下一次中继续读
                    client.receiveLength += buffer.capacity();
                    client.outChannel.write(buffer);
                    if (client.isFinished()) {
                        finished(key, client);
                    }
                    buffer.clear();
                }
            }
            // INFO 由于channel在finished方法中被销毁，
            // INFO 因此注册在selector上的相应SelectionKey也需要被取消
            key.cancel();
        } catch (IOException e) {
            key.cancel();
            e.printStackTrace();
            return;
        } finally {
            finished(key, client);
            buffer.clear();
        }
//        if (num == -1) {
//            finished(key, client);
//            buffer.clear();
//        }
    }

    private void finished(SelectionKey key, Client client) throws IOException {
        IOUtils.closeQuietly(client.outChannel);
        logger.info(sdf.format(new Date(System.currentTimeMillis())) +
                " NIO sending finished.");
        key.cancel();
        logger.debug(sdf.format(new Date(System.currentTimeMillis())) +
                " NIO file receiving succeed, name: " + client.fileName);
        logger.debug(sdf.format(new Date(System.currentTimeMillis())) +
                " NIO file receiving succeed, file size: " + client.fileLength);
        long endTime = System.currentTimeMillis();
        logger.debug(sdf.format(new Date(System.currentTimeMillis())) +
                " NIO file receiving time: " + (endTime - client.startTime));
    }
}
